﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using System.Timers;
using System.Threading;
using System.Net;
using System.Net.Sockets;

namespace NetworkingLibrary.Clients.TCP
{
    public interface ITCPClient : IClient
    {
        IPAddress LocalAddress
        {
            get;
        }
        IPAddress RemoteAddress
        {
            get;
        }

        bool FlowControl
        {
            get;
            set;
        }

        Messages.MessageObject GetMessageResponse(Messages.MessageObject MessageToSend, Type WantedResponseType);
    }
    public class TCPClient : ITCPClient
    {
        public int MaxFlowRate
        {
            get
            {
                return (int)FlowControlTimer.Interval;
            }
            set
            {
                FlowControlTimer.Interval = value;
            }
        }
        private DateTime lastSendTime = new DateTime();
        public DateTime LastSendTime
        {
            get
            {
                return lastSendTime;
            }
            set
            {
                lastSendTime = value;
            }
        }        
        private System.Timers.Timer FlowControlTimer = new System.Timers.Timer(50.0);
        private List<Messages.MessageObject> MessageQueue = new List<Messages.MessageObject>();
        private bool flowControl = true;
        public bool FlowControl
        {
            get
            {
                return flowControl;
            }
            set
            {
                if (FlowControlTimer.Enabled && !value)
                {
                    FlowControlTimer.Stop();
                }
                flowControl = value;
            }
        }

        protected int bufferSize = 100000;
        public int BufferSize
        {
            get
            {
                return bufferSize;
            }
            set
            {
                bufferSize = value;
            }
        }
        
        protected bool terminating = false;
        public bool Terminating
        {
            get
            {
                return terminating;
            }
            set
            {
                if (value)
                {
                    terminating = value;
                }
            }
        }

        private Guid id = Guid.NewGuid();
        public Guid ID
        {
            get
            {
                return id;
            }
            set
            {
                id = value;
            }
        }
        
        public bool Connected
        {
            get
            {
                return theSocket != null && theSocket.Connected;
            }
        }
        
        //Events have to be overriden so that Invoke can be called on them
        public virtual event Events.ClientConnectDelegate OnConnect = delegate { };
        public virtual event Events.ClientDisconnectDelegate OnDisconnect = delegate { };
        public virtual event Events.MessageReceivedDelegate OnMessageReceived = delegate { };
        public virtual event Events.ErrorDelegate OnError = delegate { };
        

        protected Socket theSocket;
        internal Socket TheSocket
        {
            get
            {
                return theSocket;
            }
        }

        public IPAddress LocalAddress
        {
            get
            {
                return ((IPEndPoint)TheSocket.LocalEndPoint).Address;
            }
        }
        public IPAddress RemoteAddress
        {
            get
            {
                return ((IPEndPoint)TheSocket.RemoteEndPoint).Address;
            }
        }
        
        private AsyncCallback ReceiveCallback;

        private bool DataSent = false;

        public TCPClient()
        {
        }
        public TCPClient(Events.MessageReceivedDelegate OnAMessageReceived)
        {
            OnMessageReceived += OnAMessageReceived;
        }
        public TCPClient(Socket ASocket)
        {
            theSocket = ASocket;
        }
        
        public bool Connect(int Port, System.Net.IPAddress TheAddress, bool ShouldStart = true)
        {
            try
            {
                Terminating = false;
                
                theSocket = new Socket(TheAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                theSocket.Connect(TheAddress, Port);
                Thread.Sleep(250);
                if (theSocket.Connected)
                {
                    OnConnect.Invoke(new Events.ClientConnectEventArgs(this));
                    theSocket.Blocking = false;
                    theSocket.NoDelay = true;
                    return ShouldStart ? Start() : true;
                }
            }
            catch (Exception e)
            {
                OnError.Invoke(new Events.ErrorEventArgs(e));
            }
            return false;
        }
        private bool Disconnecting = false;
        public bool Disconnect(bool IgnoreSocketState = false)
        {
            try
            {
                if (theSocket != null && (theSocket.Connected || IgnoreSocketState) && !Disconnecting)
                {
                    Disconnecting = true;
                    Stop();
                    theSocket.Close(5);
                    OnDisconnect.Invoke(new Events.ClientDisconnectEventArgs(this));
                    Disconnecting = false;
                }                
            }
            catch (Exception e)
            {
                Disconnecting = false;
                OnError.Invoke(new Events.ErrorEventArgs(e));
            }
            return theSocket == null ? true : !theSocket.Connected;
        }

        public bool Start()
        {
            try
            {
                ReceiveCallback = new AsyncCallback(Read);
                byte[] TheBuffer = new byte[BufferSize];
                theSocket.BeginReceive(TheBuffer, 0, TheBuffer.Length, SocketFlags.None, ReceiveCallback, TheBuffer);
                return true;
            }
            catch (Exception e)
            {
                OnError.Invoke(new Events.ErrorEventArgs(e));
            }
            return false;
        }
        public virtual bool Stop()
        {
            try
            {
                if (theSocket != null && theSocket.Connected)
                {
                    Terminating = true;

                    bool OK = Send(new Messages.DisconnectMessage());
                    Thread.Sleep(100);
                    return OK;
                }
                else
                {
                    return true;
                }
            }
            catch (Exception e)
            {
                OnError.Invoke(new Events.ErrorEventArgs(e));
            }
            return false;
        }

        byte[] PreviousBytes = null;
        private void Read(IAsyncResult Result)
        {
            try
            {
                int BytesRead = theSocket.EndReceive(Result);

                if (!Terminating)
                {
                    if (theSocket.Connected)
                    {
                        byte[] TheBuffer = new byte[BufferSize];
                        theSocket.BeginReceive(TheBuffer, 0, TheBuffer.Length, SocketFlags.None, ReceiveCallback, TheBuffer);
                    }
                    else
                    {
                        Stop();
                    }
                }

                if (BytesRead > 0)
                {
                    lock (LastMessagesReceived)
                    {
                        List<byte> InputBytes = new List<byte>();
                        if (PreviousBytes != null)
                        {
                            InputBytes.AddRange(PreviousBytes);
                        }
                        InputBytes.AddRange((byte[])(Result.AsyncState));
                        List<byte[]> AllMessages = GetAllMessages(InputBytes.ToArray(), out PreviousBytes);
                        for (int i = 0; i < AllMessages.Count; i++)
                        {
                            Messages.MessageObject TheMessageObject = MessageFromBytes(AllMessages[i]);

                            if (IsDisconnect(TheMessageObject))
                            {
                                Disconnect();
                            }
                            else
                            {
                                LastMessagesReceived.Add(TheMessageObject);
                                OnMessageReceived.Invoke(new Events.MessageReceivedEventArgs(TheMessageObject, this));
                            }
                        }
                    }
                }
                else
                {
                    Disconnect();
                }
            }
            catch (SocketException ex)
            {
                if (ex.ErrorCode == (int)SocketError.ConnectionReset)
                {
                    Disconnect(true);
                }
                else
                {
                    if (!theSocket.Connected)
                    {
                        Disconnect(true);
                    }
                }
            }
            catch
            {
                if (!theSocket.Connected)
                {
                    Disconnect(true);
                }
            }

            WaitForResponseEvent.Set();
        }
        
        public bool Send(byte[] Data)
        {
            try
            {
                return Send(new Messages.DataMessage(Data));
            }
            catch(Exception ex)
            {
                OnError.Invoke(new Events.ErrorEventArgs(ex));
            }
            return false;
        }
        public bool Send(Messages.MessageObject TheMessage)
        {
            if (flowControl)
            {
                try
                {
                    if (!FlowControlTimer.Enabled)
                    {
                        FlowControlTimer.Elapsed += FlowControl_QueueWorkerMethod;
                        FlowControlTimer.Start();
                    }
                    MessageQueue.Add(TheMessage);
                    return true;
                }
                catch (Exception ex)
                {
                    OnError.Invoke(new Events.ErrorEventArgs(ex));
                }
            }
            else
            {
                return DoSend(TheMessage);
            }
            return false;
        }
        private bool DoSend(Messages.MessageObject TheMessage)
        {
            try
            {
                if (theSocket.Connected)
                {
                    Utilities.Utilities.CheckFlowRate(this);

                    return DataSent = theSocket.Send(MessageToBytes(TheMessage)) > 0;
                }
                else
                {
                    Disconnect();
                    return false;
                }
            }
            catch (System.Net.Sockets.SocketException ex)
            {
                if (ex.ErrorCode != 10053)
                {
                    OnError.Invoke(new Events.ErrorEventArgs(ex));
                }
            }
            catch (Exception ex)
            {
                OnError.Invoke(new Events.ErrorEventArgs(ex));
            }
            return false;
        }

        private void FlowControl_QueueWorkerMethod(object sender, EventArgs e)
        {
            try
            {
                if (MessageQueue.Count > 0)
                {
                    if (MessageQueue[0].AttemptsToSend < 3 && DoSend(MessageQueue[0]))
                    {
                        WaitForSendEvent.Set();
                        MessageQueue.Remove(MessageQueue[0]);
                    }
                    else if (MessageQueue[0].AttemptsToSend >= 3)
                    {
                        WaitForSendEvent.Set();
                        MessageQueue.Remove(MessageQueue[0]);
                    }
                    else
                    {
                        MessageQueue[0].AttemptsToSend++;
                    }
                }
            }
            catch (Exception ex)
            {
                OnError.Invoke(new Events.ErrorEventArgs(ex));
            }
        }

        private ManualResetEvent WaitForSendEvent = new ManualResetEvent(false);
        private ManualResetEvent WaitForResponseEvent = new ManualResetEvent(false);
        private List<Messages.MessageObject> LastMessagesReceived = new List<Messages.MessageObject>();
        private List<Messages.MessageObject> WaitForResponse(int Timeout = 10000)
        {
            try
            {
                WaitForSendEvent.WaitOne(Timeout);
                if (theSocket.Connected && DataSent)
                {
                    lock (LastMessagesReceived)
                    {
                        LastMessagesReceived.Clear();
                    }
                    WaitForResponseEvent.WaitOne(Timeout);
                    WaitForResponseEvent.Reset();
                    DataSent = false;
                    lock (LastMessagesReceived)
                    {
                        return LastMessagesReceived;
                    }
                }
            }
            catch (Exception ex)
            {
                OnError.Invoke(new Events.ErrorEventArgs(ex));
            }
            return new List<Messages.MessageObject>();
        }
        public Messages.MessageObject GetMessageResponse(Messages.MessageObject MessageToSend, Type WantedResponseType)
        {
            WaitForSendEvent.Reset();
            WaitForResponseEvent.Reset();
            if (Send(MessageToSend))
            {
                List<Messages.MessageObject> PotResponses = WaitForResponse();
                if (PotResponses.Count > 0)
                {
                    List<Messages.MessageObject> FilteredResponses = (from Resps in PotResponses
                                                                      where (Resps.GetType() == WantedResponseType)
                                                                      select Resps).ToList();
                    if (FilteredResponses.Count > 0)
                    {
                        return FilteredResponses.First();
                    }
                    else
                    {
                        throw new ArgumentOutOfRangeException("FilteredResponses", "Some unrecognised responses!");
                    }
                }
                else
                {
                    throw new ArgumentOutOfRangeException("PotResponses", "No responses!");
                }
            }
            return null;
        }

        protected virtual List<byte[]> GetAllMessages(byte[] BytesRead, out byte[] RemainingBytes)
        {
            RemainingBytes = null;
            return Utilities.Utilities.GetAllMessages(BytesRead);
        }
        protected virtual Messages.MessageObject MessageFromBytes(byte[] MessageBytes)
        {
            return (Messages.MessageObject)(Utilities.Utilities.FromBytes(MessageBytes));
        }
        protected virtual byte[] MessageToBytes(Messages.MessageObject TheMessage)
        {
            return Utilities.Utilities.FormMessage(Utilities.Utilities.ToBytes(TheMessage));
        }
        protected virtual bool IsDisconnect(Messages.MessageObject TheMessage)
        {
            return TheMessage.GetType() == typeof(Messages.DisconnectMessage);
        }
    }
}
